iT邦幫忙

2026 iThome 鐵人賽

DAY 4
0
Software Development

手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫系列 第 4

Day 04:實作 RESP Parser - 解析 Simple Strings, Errors 與 Integers

  • 分享至 

  • xImage
  •  

昨天把 Parser 的骨架搭好了,今天就直接來寫解析邏輯吧。

我先挑比較軟的柿子吃,從 RESP 裡三種單行資料型態開始:Simple Strings (簡單字串)Errors (錯誤訊息)Integers (整數)。它們長度不固定,但格式都很像,最後都用 \r\n (CRLF) 收尾。


解析 Simple Strings (+)

1. 協定規範

Simple Strings 的格式為:

+<字串內容>\r\n

例如:

+OK\r\n

2. 程式碼實作

Day 3 已經先把 readLine() 寫出來,所以 Simple Strings 這邊就不用想太多:讀一行,轉成 Go 的字串,再包成 RESP Value。

// 在 code/resp/parser.go 中
func (r *Reader) readSimpleString() (Value, error) {
	line, err := r.readLine()
	if err != nil {
		return Value{}, err
	}
	return NewSimpleString(string(line)), nil
}

在這裡,我將 byte slice 直接轉換成 string 並傳給 NewSimpleString 輔助函數。


解析 Errors (-)

1. 協定規範

Errors 的格式與 Simple Strings 幾乎一模一樣,唯一不同的是prefix byte為 -

-<錯誤訊息>\r\n

例如:

-ERR unknown command\r\n

2. 程式碼實作

在 Redis 中,錯誤訊息也是一種一等公民(First-class citizen),客戶端收到以 - 開頭的訊息時,通常會丟出異常。我的解析方式同樣是讀取整行:

// 在 code/resp/parser.go 中
func (r *Reader) readError() (Value, error) {
	line, err := r.readLine()
	if err != nil {
		return Value{}, err
	}
	return NewError(string(line)), nil
}

解析 Integers (:)

1. 協定規範

Integers 的格式為:

:<整數>\r\n

例如,表示數字 1000:

:1000\r\n

表示負數 50:

:-50\r\n

2. 程式碼實作

解析整數時,需要先使用 readLine() 讀取代表數字的byte array,接著將其解析為 Go 語言中的數值型別(在 Value 結構中,我使用 int 表示)。

解析整數我直接用 Go 標準庫的 strconv.ParseInt。Redis 的整數可能不小,所以先用 10 進位、64 位元去解析,再轉成目前 Value 裡使用的 int

// 在 code/resp/parser.go 中

// readInteger 讀取行並解析為十進位整數,這個方法在解析 Bulk String 和 Array 的長度時也會被重複使用
func (r *Reader) readInteger() (int, error) {
	line, err := r.readLine()
	if err != nil {
		return 0, err
	}
	// 將位元組 slice 轉為字串並解析
	i64, err := strconv.ParseInt(string(line), 10, 64)
	if err != nil {
		return 0, fmt.Errorf("無效的整數格式: %w", err)
	}
	return int(i64), nil
}

// readIntegerType 是當prefix byte為 ':' 時的解析入口
func (r *Reader) readIntegerType() (Value, error) {
	num, err := r.readInteger()
	if err != nil {
		return Value{}, err
	}
	return NewInteger(num), nil
}

這邊我在用 ParseInt 的時候曾因為 readLine 沒有濾乾淨 \r,導致 ParseInt 一直吐 error,卡了我好幾個小時才抓到兇手,有時候 bug 藏在細節裡真討厭。


重構與封裝

透過這三個核心的子函數,ReadValue() 在收到對應的prefix byte後,就能輕鬆地分流解析:

讀取第一個 Byte(PrefixByte)
   ├── '+' ──▶ readSimpleString
   ├── '-' ──▶ readError
   └── ':' ──▶ readIntegerType

這種靠字首分流的寫法很直覺,後面要補 Bulk String 或 Array 時,只要繼續往 switch 裡加分支,不用把整個 parser 重寫。

跑起來看看

把解析器串上伺服器後,我們直接丟幾串 RESP 格式的字串給它解解看!
先啟動我們的伺服器:

$ go run ./code/main.go

打開另一個終端機,用 printf 搭配 nc 來送指令:

$ printf "+OK\r\n" | nc localhost 6379
# 預期回覆:(如果你在伺服器端有把解析完的 Value 印出來的話,會看到收到了 Simple String)

$ printf "-ERR something went wrong\r\n" | nc localhost 6379
# 預期回覆:(伺服器會解析出 Error 類型)

$ printf ":42\r\n" | nc localhost 6379
# 預期回覆:(伺服器會解析出 Integer 類型)

看到伺服器沒有 panic,而且能正確讀出字串跟整數,解析器第一階段階段性任務達成!

總結

今天先把三種單行 RESP 型別解析完。看起來只是讀字串和轉數字,但 CRLF 沒處理好就會一路壞到後面的 parser。

明天要處理比較麻煩的 Bulk Strings 和 Arrays,一想到遞迴解析就有點頭痛,明天見啦!


上一篇
Day 03:RESP 協定介紹與 Parser 基礎架構設計
系列文
手刻 Redis:用 Go 從零打造高效能高併發的記憶體資料庫4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言